package com.zhike.service.im;

import java.util.ArrayList;
import java.util.List;
import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import com.zhike.mobileimsdk.server.event.ServerEventListener;
import com.zhike.mobileimsdk.server.network.Gateway;
import com.zhike.mobileimsdk.server.protocal.Protocal;
import com.zhike.service.im.manager.ChatManager;
import com.zhike.service.im.manager.ChatManager4Group;
import com.zhike.service.im.manager.MessageRecordManager;
import com.zhike.service.im.manager.OfflineMessageManager;
import com.zhike.service.user.IUserBlacklistService;
import com.zhike.common.conf.IMBaseConf;
import com.zhike.common.constant.RedisKey;
import com.zhike.common.dto.CMDBodyC2CBeforeRequest;
import com.zhike.common.dto.CMDBodyReadedMessageRequest;
import com.zhike.common.dto.MsgBodyRoot;
import com.zhike.common.dto.UserProtocalsType;
import com.zhike.common.enums.ResultCode;
import com.zhike.common.model.Device;
import com.zhike.common.utils.DistributionSession;
import com.zhike.common.utils.JsonUtil;
import com.zhike.common.utils.RedisService;
import com.zhike.common.utils.ServerSession;
import com.zhike.common.vo.UserInfo4Cache;
import com.zhike.utils.MessageHelper;
import com.zhike.utils.MessageHelper.SendResultObserver;
import com.zhike.utils.cache.DeviceInfoCacheProvider;
import com.zhike.utils.cache.UsersInfoCacheProvider;
import com.zhike.utils.manager.UsersStatusManager;

import cn.hutool.json.JSONUtil;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTHeader;
import cn.hutool.jwt.JWTUtil;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;

/**
 * IM Server 服务器业务事件处理主实现类.
 * 
 * @version 3.0
 */
@Slf4j
@Component
public class ChatServerEventListener implements ServerEventListener {

  @Autowired
  private ChatManager chatManager;

  @Autowired
  private ChatManager4Group chatManager4Group;

  @Resource
  private MessageRecordManager messageRecordManager;

  @Resource
  private OfflineMessageManager offlineMessageManager;

  @Resource
  private MessageHelper messageHelper;

  @Resource
  private UsersInfoCacheProvider usersInfoCacheProvider;

  @Resource
  private UsersStatusManager usersStatusManager;

  @Autowired
  private DeviceInfoCacheProvider deviceInfoCacheProvider;

  @Value("${jwt.secret: default_value}")
  private String jwtSecret;

  @Autowired
  private IUserBlacklistService userBlacklistService;

  @Autowired
  private RedisService redisService;

  @Resource
  private IMBaseConf baseConf;

  /**
   * 用户的IM连接/登陆请求身份验证回调方法定义.
   * <p>
   * 服务端的应用层可在本方法中实现用户登陆验证。
   * <br>
   * 注意：本回调在一种特殊情况下——即用户实际未退出登陆但再次发起来登陆包时，本回调是不会被调用的！
   * <p>
   * 本方法中用户验证通过（即方法返回值=0时）后，将立即调用回调方法
   * {@link #onUserLoginSucess(String, String, Channel)}。
   * 否则会将验证结果（本方法返回值错误码通过客户端的 ChatBaseEvent.onLoginMessage(int dwUserId, int
   * dwErrorCode)
   * 方法进行回调）通知客户端）。
   * 
   * @param id      传递过来的准一id，保证唯一就可以通信，可能是登陆用户名、也可能是任意不重复的id等，具体意义由业务层决定
   * @param token   用于身份鉴别和合法性检查的token，它可能是登陆密码，也可能是通过前置单点登陆接口拿到的token等，具体意义由业务层决定
   * @param extra   额外信息字符串。本字段目前为保留字段，供上层应用自行放置需要的内容
   * @param session 此客户端连接对应的会话
   * @return 0 表示登陆验证通过，否则可以返回用户自已定义的错误码，错误码值应为：>=1025的整数
   */
  @Override
  public int onUserLoginVerify(String id, String token, String extra, Channel session) {
    // 校验token
    if (!JWTUtil.verify(token, jwtSecret.getBytes())) {
      log.error("{} IM连接/登陆请求时token验证失败", id);
      return ResultCode.IM_TOKEN_VERIFY.code();
    }
    // 解析Token
    final JWT jwt = JWTUtil.parseToken(token);
    jwt.getHeader(JWTHeader.TYPE);
    Object obj = jwt.getPayload("device_code");

    if (id.equals(obj.toString())) {
      log.debug("当前设备: {} 登录校验成功", obj.toString());
      return 0;
    }
    return ResultCode.IM_USERID_VERIFY.code();
  }

  /**
   * 用户的IM连接/登陆请求验证成功后的回调方法定义（即：上线通知）.
   * <p>
   * 服务端的应用层通常可在本方法中实现用户上线通知等。
   * <br>
   * 注意：本回调在一种特殊情况下——即用户实际未退出登陆但再次发起来登陆包时，回调也是一定会被调用。
   * 
   * @param deviceNo 传递过来的准一id，保证唯一就可以通信，可能是登陆用户名、也可能是任意不重复的id等，具体意义由业务层决定
   * @param extra    额外信息字符串。本字段目前为保留字段，供上层应用自行放置需要的内容。为了丰富应用层处理的手段，在本回调中也把此字段传进来了
   * @param session  成功登陆后建立的MINA会话
   */
  @Override
  public void onUserLoginSucess(String deviceNo, String extra, Channel channel) {
    log.info("[ZKIMChat] 用户连接IM服务器成功--设备ID:" + deviceNo + ",远程IP:" + channel.remoteAddress().toString());
    String gatewayFlag = Gateway.$(channel);
    int port = 0;
    if (gatewayFlag.equals("tcp")) {
      port = baseConf.getIM_SERVER_TCP_LISTENNING_PORT();
    } else if (gatewayFlag.equals("udp")) {
      port = baseConf.getIM_SERVER_UDP_LISTENNING_PORT();
    } else if (gatewayFlag.equals("websocket")) {
      port = baseConf.getIM_SERVER_WEBSOCKET_LISENNING_PORT();
    }
    ServerSession session = new ServerSession(deviceNo, channel).bind();
    redisService.set(RedisKey.sessionStore(deviceNo),
        JsonUtil
            .distubutionSession2Json(new DistributionSession(deviceNo, session.getSessionId(), port)));

    Device device = deviceInfoCacheProvider.get(deviceNo);
    if (device != null) {
      chatManager.processUserLoginAction(device.getFakeUid(), extra, channel);
    }

  }

  /**
   * 用户退出IM连接的回调方法定义（即：下线通知）。
   * <p>
   * 服务端的应用层通常可在本方法中实现用户下线通知等。
   *
   * @param session       此客户端连接对应的 netty “会话”
   * @param beKickoutCode 被踢原因编码，本参数当为-1时表示本次logout事件不是源自“被踢”，否则被踢原因编码请见
   *                      {@link com.zhike.mobileimsdk.server.protocal.s.PKickoutInfo}类中的常量定义
   * @param session       此客户端连接对应的MINA会话
   */
  @Override
  public void onUserLogout(String userId, Channel session, int beKickoutCode) {
    log.info("[ZKIMChat]用户" + (beKickoutCode != -1 ? "被踢掉(kickcode=" + beKickoutCode + ")" : "断开/注销") + "IM服务器连接--"
        + "用户ID:" + userId);
    redisService.del(RedisKey.sessionStore(userId));
    chatManager.processUserLogoutAction(userId, session, beKickoutCode);
  }

  /**
   * 收到客户端发送给“服务端”的数据回调通知（即：消息路径为“C2C”的消息）前的处理逻辑。
   * <p>
   * <b>本方法的默认实现</b>：<font color="green">当开发者不需要本方法进行额外逻辑处理时，请直接返回true即可！</font>
   * <p>
   * <b>本方法的典型用途</b>：开发者可在本方法中实现如：用户聊天内容的鉴黄、过滤、篡改等等，把内容审读权限交给开发者，就看怎么用了。
   *
   * @param p       消息/指令的完整协议包对象
   * @param session 消息发送者的“会话”引用（也就是客户端的网络连接对象）
   * @return true表示经过本方法后将正常进入
   *         {@link #onTransferMessage4C2S(Protocal, Channel)}继续正常逻辑
   *         ，false表示该条指令将不会继续处理（直接被丢弃）
   * @see #onTransferMessage4C2S(Protocal, Channel)
   * @since 8.1
   */
  @Override
  public boolean onTransferMessage4C2CBefore(Protocal p, Channel session) {
    log.debug("receive C2C message: {}", p.getDataContent());
    CMDBodyC2CBeforeRequest dto = messageHelper.parseC2CMessage(p.getDataContent());
    if (userBlacklistService.checkInBlacklist(dto.getT(), dto.getF())) {
      // 服务端推送消息给发送方，已被拉黑，无法发送消息
      try {
        SendResultObserver sendResultObserver = new SendResultObserver() {
          @Override
          public void update(boolean code, Protocal p) {
            if (code) {
              log.info("C2C 用户 {} 已被 {} 拉入黑名单,禁止发送消息", dto.getF(), dto.getT());
            }
          }
        };

        UserInfo4Cache uc = usersInfoCacheProvider.get(dto.getF());
        messageHelper.sendMessage(uc.getCurrentDeviceId(), "已被对方拉黑", false,
            UserProtocalsType.MT62_BLACKLIST, sendResultObserver);
      } catch (Exception e) {
        e.printStackTrace();
        log.error("{}", e.getMessage());
      }
      return false;
    }

    // 内容安全检查
    // if (systemConfigService.getValueByKeyAsInt("securityCheck",
    // "message").equals(YesNoEnum.YES.getValue())) {
    // TimeInterval timer = DateUtil.timer();
    // if (!AliSecurityCheckUtils.textScan(p.getDataContent(), "chat_detection")) {
    // // 发送不合法提示消息
    // try {
    // SendResultObserver sendResultObserver = new SendResultObserver() {
    // @Override
    // public void update(boolean code, Protocal p) {
    // if (code) {
    // log.info("禁止发送消息", dto.getF(), dto.getT());
    // }
    // }
    // };

    // UserInfo4Cache uc = usersInfoCacheProvider.get(dto.getF());
    // messageHelper.sendMessage(uc.getCurrentDeviceId(), "内容包含违规信息,请重新编辑", false,
    // UserProtocalsType.MT63_SECURITY, sendResultObserver);
    // } catch (Exception e) {
    // e.printStackTrace();
    // log.error("{}", e.getMessage());
    // }
    // return false;
    // }
    // log.info("消息内容安全检查耗时 {} 秒", timer.interval());
    // }
    return true;
  }

  /**
   * 收到客户端发送给“其它客户端”的数据回调通知（即：消息路径为“C2S”的消息）前的处理逻辑。
   * <p>
   * <b>本方法的默认实现</b>：<font color="green">当开发者不需要本方法进行额外逻辑处理时，请直接返回true即可！</font>
   * <p>
   * <b>本方法的典型用途</b>：开发者可在本方法中实现如：用户聊天内容的鉴黄、过滤、篡改等等，把内容审读权限交给开发者，就看怎么用了。
   *
   * @param p       消息/指令的完整协议包对象
   * @param session 消息发送者的“会话”引用（也就是客户端的网络连接对象）
   * @return true表示经过本方法后将正常进入 {@link #onTransferMessage4C2C(Protocal)}继续正常逻辑
   *         ，false表示该条指令将不会继续处理（直接被丢弃）
   * @see #onTransferMessage4C2C(Protocal)
   * @since 8.1
   */
  @Override
  public boolean onTransferMessage4C2SBefore(Protocal p, Channel session) {
    log.debug("receive C2S message: {}", p.getDataContent());
    return true;
  }

  /**
   * 通用数据回调方法定义（客户端发给服务端的（即接收user_id="0"））.
   * <p>
   * MobileIMSDK在收到客户端向user_id=0(即接收目标是服务器)的情况下通过
   * 本方法的回调通知上层。上层通常可在本方法中实现如：添加好友请求等业务实现。
   * 
   * @param p
   * @param session 此客户端连接对应的MINA会话
   * @return true表示本方法已成功处理完成，否则表示未处理成功。此返回值目前框架中并没有特殊意义，仅作保留吧
   */
  @Override
  public boolean onTransferMessage4C2S(Protocal p, Channel session) {
    // 接收者uid
    String userId = p.getTo();
    // 发送者uid
    String from_user_id = p.getFrom();
    // 消息或指令内容
    String dataContent = p.getDataContent();
    // 消息或指令指纹码（即唯一ID）
    String fingerPrint = p.getFp();
    // 【重要】用户定义的消息或指令协议类型（开发者可据此类型来区分具体的消息或指令）
    int typeu = p.getTypeu();

    log.debug("[ZKIMChat]收到了客户端" + from_user_id + "的C2S消息：str=" + dataContent);

    // 客户端向userId=0的用户发送消息，即意味着向服务端发送消息,
    // 其实OnTransBuffer_CallBack方法当且仅当客户端向userId=0的用户发送消息时被触发.
    if ("0".equals(userId)) {
      try {
        log.debug("[ZKIMChat]此消息接收对象为\"0\"(即server自已)。");
        switch (typeu) {
          // 处理用户A需要由服务端转发的群聊消息
          case UserProtocalsType.MT44_OF_GROUP$CHAT$MSG_A$TO$SERVER: {
            return chatManager4Group.processMT44_OF_GROUP$CHAT$MSG_A$TO$SERVER(fingerPrint, dataContent);
          }

          // 设置已读消息
          case UserProtocalsType.MT59_OF_MARK_READED: {
            CMDBodyReadedMessageRequest dto = messageHelper.parseReadedMessge(dataContent);
            return chatManager.processMT59_READED_MESSAGE(dto);
          }
        }
      } catch (Exception e) {
        log.error(e.getMessage(), e);
        return false;
      }
    }
    return true;
  }

  /**
   * 通道数据回调函数定义（客户端发给客户端的（即接收方user_id不为“0”的情况））.
   * <p>
   * <b>注意：</b>本方法当且仅当在数据被服务端成功在线发送出去后被回调调用.
   * <p>
   * 上层通常可在本方法中实现用户聊天信息的收集，以便后期监控分析用户的行为等^_^。
   * <p>
   * 提示：如果开启消息QoS保证，因重传机制，本回调中的消息理论上有重复的可能，请以参数 #fingerPrint
   * 作为消息的唯一标识ID进行去重处理。
   * 
   * @param p
   */
  @Override
  public void onTransferMessage4C2C(Protocal p) {
    // 接收者uid
    String userId = p.getTo();
    // 发送者uid
    String from_user_id = p.getFrom();
    // 消息或指令内容
    String dataContent = p.getDataContent();
    // 消息或指令指纹码（即唯一ID）
    String fingerPrint = p.getFp();
    // 【重要】用户定义的消息或指令协议类型（开发者可据此类型来区分具体的消息或指令）
    int typeu = p.getTypeu();

    log.debug("收到了客户端" + from_user_id + "发给客户端" + userId + "的消息：str=" + dataContent);

    // 【一对一好友聊天消息回调(可能来自桥接或本实例模式)】
    if (typeu == UserProtocalsType.MT03_OF_CHATTING_MESSAGE) {
      try {
        messageRecordManager.colectChattingMsgAsync(userId, dataContent, typeu, fingerPrint);
      } catch (Exception e) {
      }

    }

    /*******************************
     * 以下消息回调通知，只可能来自桥接模式 START
     *************************************/
    /**
     * 因为根据MobileIMSDK APP端服务端器的设计逻辑，S2C消息本实例发送成功时，
     * 由发送者在应用层处理自行决定余下逻辑（如存聊天记录），而不是由框架层的本回调处理
     */
    // 【群聊消息回调(只可能来自桥接模式)】
    // 不要在此记录群聊消息（因为群聊消息应只记录发送的，而不记录接收的，
    // 否则1000个群员就得存1000条记录，而本回调就是接收的消息，所以不应在此存）。
    // 群聊消息的聊天记录，应在发送的时候存（谁发谁存，而不是谁收谁存，详见设计备忘录！）。
    else if (typeu == UserProtocalsType.MT45_OF_GROUP$CHAT$MSG_SERVER$TO$B) {// FIXME: 以下代码仅用于Debug时，并无实际业务逻辑，可以删除！

      // 什么也不用做
      log.debug("[ZKIMChat]【Bridge2C_CallBack-群聊】[typeu=" + p.getTypeu() + "], 客户端"
          + p.getFrom() + " 发给客户端" + p.getTo() + " 的实时消息：str=[" + dataContent
          + "] 【根据S2C类型消息记录设计逻辑，此回调中无需存记录，忽略之！】");
    }
    // 【临时/陌生人聊天消息回调(只可能来自桥接模式)】
    // 陌生人消息不要在这里收集记录，根据目前的设计原则，s2c消息记录一律由发送者处理(而不是接收者)，详见设计备忘录
    else if (typeu == UserProtocalsType.MT43_OF_TEMP$CHAT$MSG_SERVER$TO$B) {// FIXME: 以下代码仅用于Debug时，并无实际业务逻辑，可以删除！

      // 什么也不用做
      log.debug("[ZKIMChat]【Bridge2C_CallBack-陌生人聊】[typeu=" + p.getTypeu() + "], 客户端"
          + p.getFrom() + " 发给客户端" + p.getTo() + " 的实时消息：str=[" + dataContent
          + "] 【根据S2C类型消息记录设计逻辑，此回调中无需存记录，忽略之！】");
    } else if (typeu == UserProtocalsType.MT17_OF_VIDEO$VOICE$REQUEST_REQUESTING$FROM$A) {

    } else if (typeu == UserProtocalsType.MT18_OF_VIDEO$VOICE$REQUEST_ABRORT$FROM$A) {

    } else if (typeu == UserProtocalsType.MT19_OF_VIDEO$VOICE$REQUEST_ACCEPT$TO$A) {

    }
    /*******************************
     * 以下消息回调通知，只可能来自桥接模式 END
     *************************************/

    else {
      log.debug("[ZKIMChat]【C2C_CallBack】[typeu=" + typeu + "], 客户端" + from_user_id + " 发给客户端"
          + userId + " 的消息：str=[" + dataContent + "] 【不支持的typeu类型，无需进行聊天记录存储！！】");
    }
  }

  /**
   * 服务端在进行消息发送时，当对方在线但实时发送失败、以及其它各种问题导致消息并没能正常发出时
   * ，将无条件走本回调通知。
   * <p>
   * 注意：本方法当且仅当在数据被服务端<u>在线发送</u>失败后被回调调用.
   * <p>
   * <b>此方法存的意义何在？</b><br>
   * 发生此种情况的场景可能是：对方确实不在线（那么此方法里就可以作为离线消息处理了）、
   * 或者在发送时判断对方是在线的但服务端在发送时却没有成功（这种情况就可能是通信错误
   * 或对方非正常通出但尚未到达会话超时时限）。<br>
   * <u>应用层在此方法里实现离线消息的处理即可！</u>
   * 
   * @param p
   * @return true表示应用层已经处理了离线消息（如果该消息有QoS机制，则服务端将代为发送一条伪应答包
   *         （伪应答仅意味着不是接收方的实时应答，而只是存储到离线DB中，但在发送方看来也算是被对方收到，只是延
   *         迟收到而已（离线消息嘛））），否则表示应用层没有处理（如果此消息有QoS机制，则发送方在QoS重传机制超时
   *         后报出消息发送失败的提示）
   */
  @Override
  public boolean onTransferMessage_RealTimeSendFaild(Protocal p) {
    return onTransferMessage_RealTimeSendFaild_impl(p, true);
  }

  /**
   * onTransferMessage_RealTimeSendFaild的实现方法。
   * 
   * @param p                                  原始数据
   * @param forceFriendChattingNoRecordHistory true表示强制不存储一对一好友聊天的聊天记录，否则存储
   * @return true表示已妥善处理离线消息
   */
  public boolean onTransferMessage_RealTimeSendFaild_impl(Protocal p, boolean forceFriendChattingNoRecordHistory) {
    // 接收者uid
    String userId = p.getTo();
    // 发送者uid
    String from_user_id = p.getFrom();
    // 消息或指令内容
    String dataContent = p.getDataContent();
    // 消息或指令指纹码（即唯一ID）
    String fingerPrint = p.getFp();
    // 【重要】用户定义的消息或指令协议类型（开发者可据此类型来区分具体的消息或指令）
    int typeu = p.getTypeu();

    // 此离线消息是否需要记录到聊天记录中
    boolean needRecordToChattingHistory = false;

    boolean processed = false;

    switch (typeu) {
      // 目前C2C的消息需要离线处理的，也就是用户的一对一好友聊天消息了
      case UserProtocalsType.MT03_OF_CHATTING_MESSAGE: {
        // processed = OfflineMessageManager.processOfflineMessage(p, false);

        // 强制不存储聊天记录（因为一对一好友聊天走的是c2c模式，它的聊天记录在QoS模
        // 式下已走 onTransferMessage4C2C()回调进行了存储，此参数就是为了在QoS场景下
        // 不需要重复存储聊天记录了）
        if (forceFriendChattingNoRecordHistory) {
          needRecordToChattingHistory = false;
        } else {
          /**
           * 根据MobileIMSDK和Web端服务端器的设计逻辑，C2C好友聊天消息在接收方离线情况下的聊天记录处理，
           * 需要在离线回调中处理（而不是在onTransBuffer_C2C_CallBack中），详见“RainbowChat_pro_v4关键设计思路备忘.txt”
           */
          needRecordToChattingHistory = true;
        }
        //
        // break;
      }
      /*******************************
       * 以下离线处理，只可能来自桥接模式 START
       *************************************/
      /** 因为根据MobileIMSDK APP端服务端器的设计逻辑，S2C消息本实例发送失败时，由发送者在应用层处理，而不是由框架层的本回调处理 */
      // * 临时聊天消息的离线处理逻辑
      case UserProtocalsType.MT43_OF_TEMP$CHAT$MSG_SERVER$TO$B:
        // * 群组聊天消息的离线处理逻辑
      case UserProtocalsType.MT45_OF_GROUP$CHAT$MSG_SERVER$TO$B:
        // * 各群聊指令通知的离线处理逻辑
      case UserProtocalsType.MT46_OF_GROUP$SYSCMD_MYSELF$BE$INVITE_FROM$SERVER:
      case UserProtocalsType.MT47_OF_GROUP$SYSCMD_COMMON$INFO_FROM$SERVER:
      case UserProtocalsType.MT48_OF_GROUP$SYSCMD_DISMISSED_FROM$SERVER:
      case UserProtocalsType.MT49_OF_GROUP$SYSCMD_YOU$BE$KICKOUT_FROM$SERVER:
      case UserProtocalsType.MT50_OF_GROUP$SYSCMD_SOMEONEB$REMOVED_FROM$SERVER:
      case UserProtocalsType.MT51_OF_GROUP$SYSCMD_GROUP$NAME$CHANGED_FROM$SERVER: {
        log.debug("---------------转到离线消息处理1-------------- {}", p.toGsonString());
        processed = offlineMessageManager.processOfflineMessage(p, false);
        break;
      }
      /*******************************
       * 以上离线处理，只可能来自桥接模式 END
       *************************************/

      default: {
        log.debug("[ZKIMChat]【C2C】客户端" + from_user_id + "发给" + userId + "的消息类型typeu为"
            + typeu + "的消息或指令：str=" + dataContent + "没有在离线处理逻辑内，本次离线回调被忽略哦！");
        processed = true;
        break;
      }
    }

    // 离线消息也要记录到聊天记录中
    if (needRecordToChattingHistory) {
      log.debug("-------------离线消息 -------- {}", p.getDataContent());
      try {
        // ** 【用户消息收集并落库】收集用户的聊天消息，以供后台进行用户行为分析，
        messageRecordManager.colectChattingMsgAsync(userId, p.getDataContent(), typeu, p.getFp());
      } catch (Exception e) {
        log.warn(e.getMessage(), e);
      }
    }

    return processed;
  }

  /**
   * <b>注意：</b><font color=
   * "red">本回调仅用于与Web的互通模式下，默认情况下本方法可什么也不做，无任何影响。如你对此回调有疑问可跟Jack
   * Jiang进行技术讨论！</font>
   * {@inheritDoc}
   *
   * @since 8.1
   */
  @Override
  public void onTransferMessage4C2C_AfterBridge(Protocal p) {
    // 默认本方法可
  }

  public UsersStatusManager getLiverUsersMgr() {
    return usersStatusManager;
  }

  @Override
  public List<Protocal> onTransferMessage4C2C(Protocal p, Channel session) {

    String fromUid = p.getFrom();
    // 接收者uid
    String userId = p.getTo();

    // 消息或指令指纹码（即唯一ID）
    String fingerPrint = p.getFp();

    String dataContent = p.getDataContent();

    // 【重要】用户定义的消息或指令协议类型（开发者可据此类型来区分具体的消息或指令）
    int typeu = p.getTypeu();
    List<Device> receiverDevices = new ArrayList<>();

    List<Protocal> protocals = new ArrayList<>();
    UserInfo4Cache uc = usersInfoCacheProvider.get(userId);

    for (Device device : uc.getDeviceList()) {
      receiverDevices.add(device);
    }

    // 将发送者其他终端设备加入到接受设备列表中
    MsgBodyRoot root = MessageHelper.pareseChatMsg_ROOT_Message(dataContent);

    UserInfo4Cache uc2 = usersInfoCacheProvider.get(fromUid);
    if (uc2 != null) {
      for (Device device : uc2.getDeviceList()) {
        if (device.getDeviceNo().equals(root.getM2())) {
          log.debug("发送目标设备时被屏蔽: {}", device.getDeviceNo());
          continue;
        }
        if (typeu == UserProtocalsType.MT17_OF_VIDEO$VOICE$REQUEST_REQUESTING$FROM$A
            || typeu == UserProtocalsType.MT18_OF_VIDEO$VOICE$REQUEST_ABRORT$FROM$A
            || typeu == UserProtocalsType.MT19_OF_VIDEO$VOICE$REQUEST_ACCEPT$TO$A) {
          continue;
        }
        if (usersStatusManager.isOnline(device.getDeviceNo())) {
          receiverDevices.add(device);
        }
      }
    }

    log.debug("消息目标设备列表: {}", receiverDevices);

    // 从缓存中取用户所有设备，构造protocal返回
    for (Device device : receiverDevices) {
      Protocal cp = new Protocal();
      cp.setBridge(false);
      cp.setQoS(false);
      cp.setSm(-1);
      cp.setTo(device.getDeviceNo());
      cp.setType(2);
      cp.setTypeu(p.getTypeu());
      cp.setFrom(p.getFrom());
      cp.setDataContent(p.getDataContent());
      cp.setFp(fingerPrint); // 这里一定是接收指纹码
      protocals.add(cp);
      log.debug("单聊: {}", JSONUtil.toJsonStr(cp));
    }

    return protocals;
  }
}
